//////////////////////////////////////////////////
// glShaderProgram.cpp
//
// Author: Tarik Belabbas et Adrien Hoffer
//////////////////////////////////////////////////

#include <fstream>
#include <iostream>

#include <glm/glm.hpp>
#include <glm/gtc/type_ptr.hpp> // glm::value_ptr
#include "glShaderProgram.h"
#include "../common/Common.h"

using namespace std;

//------------------------------------------------------------------------------
glShaderProgram::glShaderProgram()
{
	m_program			= 0;

	m_vertexShader.m_shaderType		= GL_VERTEX_SHADER;
	m_fragmentShader.m_shaderType	= GL_FRAGMENT_SHADER;
	m_geometryShader.m_shaderType	= GL_GEOMETRY_SHADER;
}

//------------------------------------------------------------------------------
glShaderProgram::~glShaderProgram()
{
	unload();
}

//------------------------------------------------------------------------------
GLboolean glShaderProgram::initialize( const char* _vertexPath,
		const char* _fragmentPath/* = NULL*/, const char* _geometryPath/* = NULL*/ )
{
	// Loading shaders
	if ( loadShader( m_vertexShader, _vertexPath ) )
	{
		if ( _fragmentPath == NULL || loadShader( m_fragmentShader, _fragmentPath ) )
		{
			if ( _geometryPath == NULL || loadShader( m_geometryShader, _geometryPath ) )
			{
				// Creating program and attaching shaders to it
				m_program = glCreateProgram();

				glAttachShader( m_program, m_vertexShader.m_shader );

				if ( _fragmentPath != NULL )
				{
					glAttachShader( m_program, m_fragmentShader.m_shader );
				}

				if ( _geometryPath != NULL )
				{
					glAttachShader( m_program, m_geometryShader.m_shader );

					// Geometry shader parameters, max can be kept 1024 and not
					// be worried about at all
					glProgramParameteriARB( m_program,
							GL_GEOMETRY_VERTICES_OUT, 6 );

					// CPU must send primitives of following type below the pipeline
					glProgramParameteriARB( m_program, GL_GEOMETRY_INPUT_TYPE,
							GL_TRIANGLES );

					// GS can generate more vertices and of a different primitive type
					glProgramParameteriARB( m_program,
							GL_GEOMETRY_OUTPUT_TYPE, GL_TRIANGLES );
				}

				return linkProgram();
			}
			else
			{
				unloadShader( m_vertexShader.m_shader );
				unloadShader( m_fragmentShader.m_shader );
			}
		}
		else
		{
			unloadShader( m_vertexShader.m_shader );
		}
	}

	return false;
}

//------------------------------------------------------------------------------
bool glShaderProgram::loadShader( glSShaderObject& _shader,
		const char* _shaderPath )
{
	// Reading file
	ifstream shaderFile( _shaderPath );
	if ( shaderFile.good() )
	{
		// Creating file stream and loading file content
		string stringBuffer( istreambuf_iterator< char >( shaderFile ),
					( istreambuf_iterator< char >() ) );
		shaderFile.close();

		const char* buffer = stringBuffer.c_str();

		_shader.m_shader = glCreateShader( _shader.m_shaderType );

		// Sending the source, NULL indicates that the string is '\0'-terminated
		glShaderSource( _shader.m_shader, 1, &buffer, NULL );

		// Compiling shader
		glCompileShader( _shader.m_shader );
		GLint result;
		glGetShaderiv( _shader.m_shader, GL_COMPILE_STATUS, &result );

		// Checking shader compilation's result
		if ( result == GL_TRUE )
		{
			return true;
		}
		else
		{
			// Writing debug output if compilation failed
			string infoLog;
			GLint infoLength;
			glGetShaderiv( _shader.m_shader, GL_INFO_LOG_LENGTH, &infoLength );
			infoLog.resize( infoLength );

			glGetShaderInfoLog( _shader.m_shader, sizeof( infoLog ), &infoLength,
					&infoLog[ 0 ] );

			ST_ERROR( ( "Could not compile shader: %s\nCompile error:",
					_shaderPath, infoLog.c_str() ) );

			return false;
		}
	}
	else
	{
		ST_ERROR( ( "Could not load shader: %s", _shaderPath ) );

		return false;
	}
}

//------------------------------------------------------------------------------
void glShaderProgram::unload()
{
	// Unloading valid shaders and setting corresponding ids to 0
	if ( m_program != 0 )
	{
		if ( m_vertexShader.m_shader != 0 )
		{
			unloadShader( m_vertexShader.m_shader );
		}

		if ( m_fragmentShader.m_shader != 0 )
		{
			unloadShader( m_fragmentShader.m_shader );
		}

		if ( m_geometryShader.m_shader != 0 )
		{
			unloadShader( m_geometryShader.m_shader );
		}

		glDeleteShader( m_program );
	}
}

//------------------------------------------------------------------------------
void glShaderProgram::unloadShader( GLuint& _shader )
{
	glDetachShader( m_program, _shader );
	glDeleteShader( _shader );
	_shader = 0;
}

//------------------------------------------------------------------------------
bool glShaderProgram::linkProgram()
{
	// Linking program and checking link status
	glLinkProgram( m_program );
	GLint result;
	glGetProgramiv( m_program, GL_LINK_STATUS, &result );

	if ( result == GL_TRUE )
	{
		// Ready to go!
		//glUseProgram( m_program );

		return true;
	}
	else
	{
		return false;
	}
}

//------------------------------------------------------------------------------
void glShaderProgram::useProgram()
{
	glUseProgram( m_program );
}

//------------------------------------------------------------------------------
void glShaderProgram::unuseProgram()
{
	glUseProgram( NULL );
}

//------------------------------------------------------------------------------
GLuint glShaderProgram::getUniformLocation( const char* _name )
{
   map< string, GLuint >::iterator it = m_uniformMap.find( _name );

	if ( it == m_uniformMap.end() )
	{
		GLuint location = glGetUniformLocation( m_program, _name );
		m_uniformMap.insert( make_pair( _name, location ) );

		return location;
	}

	return ( *it ).second;
}

//------------------------------------------------------------------------------
GLuint glShaderProgram::getAttribLocation( const char* _name )
{
   map< string, GLuint >::iterator it = m_attribMap.find( _name );

	if ( it == m_attribMap.end() )
	{
		GLuint location = glGetAttribLocation( m_program, _name );
		m_attribMap.insert( make_pair( _name, location ) );

		return location;
	}

	return ( *it ).second;
}

//------------------------------------------------------------------------------
void glShaderProgram::sendUniform( const char* _name, const float* _matrix,
		bool _transpose /*= false*/)
{
	GLuint location = getUniformLocation( _name );
	glUniformMatrix4fv( location, 1, _transpose, _matrix );
}

//------------------------------------------------------------------------------
void glShaderProgram::sendUniform( const char* _name, float _uniform )
{
	GLuint location = getUniformLocation( _name );
	glUniform1f( location, _uniform );
}

//------------------------------------------------------------------------------
void glShaderProgram::sendUniform( const char* _name, int _uniform )
{
	GLuint location = getUniformLocation( _name );
	glUniform1i( location, _uniform );
}

//------------------------------------------------------------------------------
void glShaderProgram::sendUniform( const char* _name, glm::vec3 _uniform )
{
	GLuint location = getUniformLocation( _name );
	glUniform3fv( location, 1, glm::value_ptr( _uniform ) );
}

//------------------------------------------------------------------------------
void glShaderProgram::bindAttribute( unsigned int _index, const char* _attribName )
{
	glBindAttribLocation( m_program, _index, _attribName );
}

//------------------------------------------------------------------------------
void glShaderProgram::transformFeedbackVaryings( GLsizei _count,
		const GLchar** _varyings, GLenum _bufferMode)
{
	glTransformFeedbackVaryings( m_program, _count, _varyings, _bufferMode );
}
